import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { Overlay, OverlayConfig, ViewportRuler } from '@angular/cdk/overlay';
import { Portal } from '@angular/cdk/portal';
import { doFnResult } from '../utils/result';
import { TypeUtils } from '../utils/type';
import { ExtendOverlayRef } from './extendOverlayRef';
import { ExtendOverlayConfig } from './interface';

@Injectable()
export class OverlayService {
  private _listenerClick;
  private _listenerScroll;
  private renderer: Renderer2;
  private _listenerMousewheel;

  constructor(
    private overlay: Overlay,
    private rendererFactory: RendererFactory2,
    private viewportRuler: ViewportRuler,
  ) {
    this.renderer = rendererFactory.createRenderer(null, null);
  }

  getTargetNode(ele, target): boolean {
    if (!ele || ele === document) {
      return false;
    }
    return ele === target ? true : this.getTargetNode(ele.parentNode, target);
  }

  isExcludedNode(ele, exclusions): boolean {
    let exclusionList = exclusions;
    if (typeof exclusions === 'string') {
      exclusionList = document.querySelectorAll(exclusions);
    }
    return Array.from(exclusionList).some((item): boolean => this.getTargetNode(ele, item) === true);
  }

  open(portal: Portal<any>, config?: OverlayConfig, extendConfig?: ExtendOverlayConfig): ExtendOverlayRef {
    const CONFIG: OverlayConfig = this._getOverlayConfig(config);
    const EXTEND_CONFIG = { clickToCloseStrategy: true, scrollToCloseStrategy: true, ...extendConfig };

    if (EXTEND_CONFIG?.beforeOpen) {
      // 打开 overlay 之前的回调函数
      const call = EXTEND_CONFIG.beforeOpen();
      doFnResult(
        call,
        (res): ExtendOverlayRef => (res ? this._attatchOverlay(portal, CONFIG, EXTEND_CONFIG) : undefined),
      );
    } else {
      return this._attatchOverlay(portal, CONFIG, EXTEND_CONFIG);
    }
    // @ts-expect-errors 火狐异常
    if (typeof InstallTrigger !== 'undefined') {
      // 火狐浏览器缩放时候，body和viewport的大小不一致
      // document.body.scrollHeight(scrollWidth)和window.innerHeight(innerWidth)大小不一样
      // 添加cdk-global-scrollblock导致html元素position变化
      // 在火狐浏览器中position变更，会导致已滚动的滚动元素触发一次scroll事件
      // 致使overlay自动关闭，表现为弹层无法正常打开
      document.documentElement.classList.add('cdk-firefox-polyfill');
    }
    return undefined;
  }

  private convertWindow(ele: HTMLElement): Window {
    return ele?.ownerDocument?.defaultView || window;
  }

  private isScrollElement(element: HTMLElement): boolean {
    const scrollStyle = new Set<string>(['hidden', 'scroll', 'overlay', 'auto']);
    const { overflowX, overflowY, overflow } = this.convertWindow(element).getComputedStyle(element);
    return [overflowX, overflowY, overflow].some((o): boolean => scrollStyle.has(o));
  }

  private _getOverlayConfig(config?: OverlayConfig): OverlayConfig {
    const positionStrategy = this.overlay.position().global().centerHorizontally().centerVertically();
    const scrollStrategy = this.overlay.scrollStrategies.block();

    const DEFAULT_CONFIG = new OverlayConfig({
      hasBackdrop: true,
      backdropClass: 'cdk-overlay-dark-backdrop',
      positionStrategy,
      scrollStrategy,
    });

    return { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * @param ele - 目标元素
   * @param excludeClass - 排除元素class
   * @param onlyScrollElement - 是否只获取有滚动条的父级元素
   * @returns 返回所有可能的父级元素
   */
  private getParents(
    ele: HTMLElement,
    exclusions: HTMLElement[] | string,
    onlyScrollElement: boolean = false,
  ): HTMLElement[] {
    const parentList: HTMLElement[] = [];
    let current = ele;
    do {
      current = current?.parentElement;
      if (!current || this.isExcludedNode(current, exclusions)) {
        continue;
      }
      if (!onlyScrollElement || (onlyScrollElement && this.isScrollElement(current))) {
        parentList.push(current);
      }
    } while (current);

    return parentList;
  }

  private _listenEvent(e, extendOverlayRef, config, extendConfig): void {
    if (!extendOverlayRef.overlayRef.overlayElement) {
      return;
    }

    // If page is in the global loading state, stop closing
    const loadingLayer = document.querySelector('.lv-loading-gloabl-container');
    const isGlobalLoading = !(loadingLayer === undefined || loadingLayer === null);
    if (isGlobalLoading) {
      return;
    }
    const doClose = this._shouldClose(e, extendOverlayRef, config, extendConfig);

    if (doClose) {
      extendConfig.internalTriggerClose ? extendConfig.internalTriggerClose(e) : extendOverlayRef.close(e);
    }
  }

  private listenParentScrollFromOrigin(
    extendOverlayRef: ExtendOverlayRef,
    config?: OverlayConfig,
    extendConfig?: ExtendOverlayConfig,
  ): void {
    this.getParents(
      (config.positionStrategy as any)._origin?.nativeElement ||
        (config.positionStrategy as any)._origin ||
        document.body,
      TypeUtils.isObject<{ exclusions: HTMLElement[] | string }>(extendConfig.scrollToCloseStrategy)
        ? extendConfig.scrollToCloseStrategy.exclusions
        : [],
      true,
    ).forEach((item): void => {
      extendOverlayRef.listeners.push(
        this.renderer.listen(item, 'scroll', (event): void => {
          this._listenEvent(event, extendOverlayRef, config, extendConfig);
        }),
      );
    });
  }

  private _shouldClose(e, extendOverlayRef, config, extendConfig): boolean {
    let doClose = false;
    const isChild = this.getTargetNode(e.srcElement, extendOverlayRef.overlayRef.overlayElement);
    const isOrigin = config.positionStrategy?._origin
      ? this.getTargetNode(
          e.srcElement,
          config.positionStrategy._origin.nativeElement || config.positionStrategy._origin,
        )
      : false;

    if (e.type === 'mousedown') {
      let isExcludedNode = false;
      if (
        extendConfig.clickToCloseStrategy instanceof Object &&
        extendConfig.clickToCloseStrategy.exclusions &&
        extendConfig.clickToCloseStrategy.exclusions.length
      ) {
        isExcludedNode = this.isExcludedNode(e.srcElement, extendConfig.clickToCloseStrategy.exclusions);
      }
      doClose = !(isChild || isOrigin || isExcludedNode);
    }

    if (e.type === 'scroll' || e.type === 'mousewheel' || e.type === 'DOMMouseScroll') {
      const el = document.querySelector('.cdk-global-scrollblock');
      const isBlockScroll = !(el === undefined || el === null);
      let isExcludedNode = false;
      if (extendConfig.scrollToCloseStrategy instanceof Object) {
        isExcludedNode = this.isExcludedNode(e.srcElement, extendConfig.scrollToCloseStrategy.exclusions);
        doClose = !(isExcludedNode || (isBlockScroll && e.srcElement === document));
      } else if (e.type === 'scroll') {
        doClose = !(isChild || isBlockScroll);
      } else {
        doClose = !isChild;
      }
    }
    return doClose;
  }

  private _attatchOverlay(
    portal: Portal<any>,
    config?: OverlayConfig,
    extendConfig?: ExtendOverlayConfig,
  ): ExtendOverlayRef {
    // 挂载 overlay portal
    const overlayRef = this.overlay.create(config);
    const _portal = overlayRef.attach(portal);

    const extendOverlayRef = new ExtendOverlayRef(overlayRef);
    extendOverlayRef.extendConfig = extendConfig;
    extendOverlayRef.portal = _portal;

    if (config.hasBackdrop && extendConfig.backdropBindClose) {
      overlayRef.backdropClick().subscribe((e): void => {
        extendConfig.internalTriggerClose ? extendConfig.internalTriggerClose(e) : extendOverlayRef.close(e);
      });
    }

    if (extendConfig.clickToCloseStrategy && !config.hasBackdrop) {
      let isListenBody = false;
      this._listenerClick = this.renderer.listen(document.body, 'mousedown', (event): void => {
        this._listenEvent(event, extendOverlayRef, config, extendConfig);
        isListenBody = true;
      });
      extendOverlayRef.listeners.push(this._listenerClick);

      // 当点击元素阻止冒泡时，无法监听body事件且需要关闭overlay，传入点击元素选择器，如id，class（不传dom节点避免异步时无法获取正确的dom）
      if (
        extendConfig.clickToCloseStrategy instanceof Object &&
        extendConfig.clickToCloseStrategy.includes &&
        extendConfig.clickToCloseStrategy.includes.length
      ) {
        if (isListenBody) {
          return undefined;
        }

        const { includes } = extendConfig.clickToCloseStrategy;
        this._handleIncludes(includes, extendOverlayRef, config, extendConfig);
      }
    }

    if (extendConfig.scrollToCloseStrategy && !config.hasBackdrop) {
      this.listenParentScrollFromOrigin(extendOverlayRef, config, extendConfig);
      extendOverlayRef.listeners.push(this._listenerMousewheel);
      extendOverlayRef.listeners.push(this._listenerScroll);
    }

    // 打开 overlay 之后的回调函数
    extendConfig.afterOpen?.(extendOverlayRef, _portal);

    return extendOverlayRef;
  }

  private _handleIncludes(includes, extendOverlayRef, config, extendConfig): void {
    let listenerClick;
    includes.forEach((item): void => {
      const doms = document.querySelectorAll(item);

      if (!doms.length) {
        return;
      }

      doms.forEach((dom): void => {
        listenerClick = this.renderer.listen(dom, 'mousedown', (event): void => {
          this._listenEvent(event, extendOverlayRef, config, extendConfig);
        });

        extendOverlayRef.listeners.push(listenerClick);
      });
    });
  }
}
